import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
// import { store } from '@/redux/store'
// import { addLog, clearLog } from '@/redux/logSlice'
import dayjs from 'dayjs'
import { create as createIpfsClient } from 'ipfs-http-client'
// import { setSeedHex, setIpfsId, setIpfsUrl, setDidStr, setOrbitdbStatus } from '@/redux/networkSlice'
import { v4 as uuidv4 } from 'uuid';
import { multiaddr } from '@multiformats/multiaddr'
import Events from 'events'
import Logger from 'logplease'
import pMap from 'p-map';
import pEachSeries from 'p-each-series'

import { seedToHex, seedHexToSeed, generateMnemonic, mnemonicToSeed, initDid, peerIdToDid, addEncryptedData, decryptJWE, isValidDid } from './did-lib'
import { createNode } from './create-node'

import Database from './dexie'

const logger = Logger.create('network')
Logger.setLogLevel('DEBUG')

Events.EventEmitter.defaultMaxListeners = 200

const HEART_BEAT_TIME = 10000

let node
let nodeId
let ipfs;
let ipfsId;
let _Did;
let _Profile
let seed
let seedHex
let mnemonic
let relayAddress
let swarmKey

let _DB
let _HeartBeatTime = Date.now() - HEART_BEAT_TIME

const BOOTSTRAP_TIME = 20000
export const isOnline = () => {
  return Date.now() - _HeartBeatTime < HEART_BEAT_TIME + 300
}

setInterval(() => {
  console.log('is online:', isOnline())
}, 5000)

let intervalId_bootstrap // bootstrap
let intervalId_heartbeat // heart beat

const IPFS_URL = 'https://libp.we-linkin.com:8443'

const DEFAULT_ADDRESS = [
  '/dns4/webrtc.we-linkin.com/tcp/443/wss/p2p-webrtc-star/'
]
// const RELAY_ADDRESS = '/dns4/libp.we-linkin.com/tcp/44003/wss/p2p/12D3KooWEAKYTNcPaEyp2Sr6PWj6bhbXo9MpgmnJ8aFYBdyKK4Yh'
const RELAY_ADDRESS = '/dns4/libp.we-linkin.com/tcp/44008/wss/p2p/12D3KooWELrypWhpyUef9gtJr5TUne1173fG24QpMrRN62HN4VDk'
// const RELAY_ADDRESS = '/ip4/122.112.230.226/tcp/4008/ws/p2p/12D3KooWELrypWhpyUef9gtJr5TUne1173fG24QpMrRN62HN4VDk'
// const RELAY_ADDRESS = '/ip4/192.168.50.26/tcp/4008/ws/p2p/12D3KooWELrypWhpyUef9gtJr5TUne1173fG24QpMrRN62HN4VDk'
const swarmKey_dev = '/key/swarm/psk/1.0.0/\n/base16/\n' + '7ba72b5b15148919d364693b0a8cd2c36a09a3345f210c96ccc019e2ace1def6'

let bootstraps = [
  RELAY_ADDRESS
]
const isRelayPeers = (peerId) => {
  return bootstraps.find(item => item.includes(peerId))
}

export const login = async (config, options = { relayAddress: RELAY_ADDRESS, swarmKey: swarmKey_dev }) => {
  let { nodeId, ipfsId } = await _initNetwork(config, options)

  _Profile = await loadProfile(_Did.id)
  let profile = await fetchProfile(_Did.id)
  if (profile) {
    await updateLocalProfile(profile)
    _Profile = profile
  } else {
    if (!_Profile) { // local DB exist
      _Profile = {
        did: _Did.id,
        nickName: 'NICKNAME_UNKNOW'
      }
    }
    _announceProfile()
  }

  logger.debug('self', _Profile)

  _updateFriendData()

  return { nodeId, ipfsId, ..._Profile }
}

export const initialize = async (config, options = { relayAddress: RELAY_ADDRESS, swarmKey: swarmKey_dev }) => {
  let { nodeId, ipfsId } = await _initNetwork(config, options)
  _Profile = { did: _Did.id, nickName: config.nickName }
  if (_Profile.nickName) {
    await updateLocalProfile(_Profile)
    _announceProfile()
  }
  logger.debug('self', _Profile)

  _updateFriendData()

  return { nodeId, ipfsId, ..._Profile }
}

const _initNetwork = async (config, options = { relayAddress: RELAY_ADDRESS, swarmKey: swarmKey_dev }) => {
  logger.debug('init network', '#' + config.mnemonic + '#')
  mnemonic = config.mnemonic
  relayAddress = options.relayAddress || RELAY_ADDRESS
  bootstraps = [relayAddress]
  swarmKey = uint8ArrayFromString(options.swarmKey)

  const swarmAddresses = DEFAULT_ADDRESS
  seed = await mnemonicToSeed(mnemonic)
  seedHex = seedToHex(seed)
  logger.debug('seed hex', seedHex)

  // init ipfs
  // ipfs = await createIpfs({ seed, swarmAddresses, swarmKey });
  ipfs = createIpfsClient({ url: IPFS_URL })
  const ipfsIdObj = await ipfs.id();
  logger.debug(ipfsIdObj)
  ipfsId = ipfsIdObj.id.toString();

  // init did
  _Did = await initDid(seed);
  logger.debug('did: ', _Did.id)

  // init IDB
  _DB = new Database({ dbName: _Did.id })

  // test export/import
  // const blob = await _DB.exportDB()
  // console.log('export data', blob)

  node = await startLibp2pNode(swarmKey, seed)
  nodeId = node.peerId.toString()

  return { nodeId, ipfsId }
}

export const restoreNetwork = async () => {
  // dobootstrap(false);
  await closeNetwork();
  await startLibp2pNode(swarmKey, seed)

  _announceProfile()
}

export const startLibp2pNode = async (swarmKey, seed) => {
  logger.debug('start libp2p node')
  // close previous libp2p node

  // create libp2p node
  node = await createNode({ swarmKey, seed })

  nodeId = node.peerId.toString()
  logger.debug('libp2p id: ', nodeId)

  handleMessages()

  // beginSync()
  beginHeartBeat(HEART_BEAT_TIME)

  dobootstrap(false);
  // keepConnection(BOOTSTRAP_TIME)

  // Listen for new connections to peers
  node.addEventListener('peer:connect', async (evt) => {
    const remotePeer = evt.detail
    let peerId = remotePeer.toString()
    logger.debug(`Connected to libp2p peer ${peerId}`)
    if (isRelayPeers(peerId)) {
      logger.debug('connected to relay node, begin sync')
      // handleMessages()
      // node.services.pubsub.addEventListener('message', messageHandler)
      // await beginSync()
      // beginHeartBeat(HEART_BEAT_TIME)

      beginSync()

      // clearInterval(intervalId_2)
      // dobootstrap(false)
      // keepConnection(BOOTSTRAP_TIME)
    }
    let connections = node.components.connectionManager.connections
    logger.debug('connections', connections.size)
    logger.debug(connections.map.forEach((value, key) => {
      console.log(key, value)
    }))
  })

  // Listen for peers disconnecting
  node.addEventListener('peer:disconnect', (evt) => {
    const remotePeer = evt.detail
    let peerId = remotePeer.toString()
    logger.debug(`Disconnected from libp2p peer ${peerId}`)
    if (isRelayPeers(peerId)) {
      logger.debug('disconnected from relay node')
    }
  })

  await waitForChannelReady()

  window.node = node
  return node
}

export const closeNetwork = async () => {
  logger.debug('close network')

  if (intervalId_bootstrap) clearInterval(intervalId_bootstrap)
  if (intervalId_heartbeat) clearInterval(intervalId_heartbeat)

  if (node) {
    node.services.pubsub.removeEventListener('message', messageHandler)
    await node.stop()
  }

  // nickName = null
  // mnemonic = null
  // _Did = null
  // window.ipfs = null

  // store.dispatch(setSeedHex(''))
  // store.dispatch(setIpfsId(''))
  // store.dispatch(setIpfsUrl(''))
  // store.dispatch(setDidStr(''))
  // store.dispatch(setOrbitdbStatus('idle'))
}

const loadProfile = async (did) => {
  const profile = await _DB.getProfile(did)
  logger.debug('local profile', profile)
  return profile
}

// if reconnect is true, it'll first attempt to disconnect from the bootstrap nodes
async function dobootstrap(reconnect) {
  logger.debug('dobootstrap', reconnect)

  for (let i in bootstraps) {
    try {
      logger.debug('dial to bootstrap', bootstraps[i])
      const addr = await node.dial(multiaddr(bootstraps[i]))
      logger.debug('connected to relay', bootstraps[i])
      const address = node.getMultiaddrs()
      for (let item of address) {
        logger.debug('multi address', item.toString())
        // relay address /dns4/webrtc.we-linkin.com/tcp/443/wss/p2p-webrtc-star/p2p/12D3KooWNAvDa9tt5pnmTEsajx4j8TDXieAH5B3PGBiG1C7VJQkQ
      }
    } catch (e) {
      logger.debug('dial err', e)
    }
  }
}

const beginHeartBeat = (interval) => {
  logger.debug('begin heart beat')
  node.services.pubsub.publish(`${_Did.id}-channel`, new TextEncoder().encode(JSON.stringify({ type: 'heart-beat' })));
  intervalId_heartbeat = setInterval(() => {
    logger.debug('send heart beat')
    node.services.pubsub.publish(`${_Did.id}-channel`, new TextEncoder().encode(JSON.stringify({ type: 'heart-beat' })));
  }, interval)
}

const stopHeartBeat = () => {
  logger.debug('stop heart beat')
  if (intervalId_heartbeat) clearInterval(intervalId_heartbeat)
}

const keepConnection = async (interval) => {
  intervalId_bootstrap = setInterval(() => {
    dobootstrap(false);
  }, interval);
}

const waitForChannelReady = async () => {
  node.services.pubsub.subscribe(`${_Did.id}-channel`)
  let waitIntervalId_2
  const waitChannelReady = () => {
    return new Promise((resolve, reject) => {
      logger.debug('wait for channal is ready: ' + _Did.id)
      waitIntervalId_2 = setInterval(() => {
        node.services.pubsub.subscribe(`${_Did.id}-channel`)
        const peers = node.services.pubsub.getSubscribers(`${_Did.id}-channel`)
        logger.debug('channel peers', peers.map(item => item.toString()))
        if (peers.length > 0) {
          logger.debug('channel is ready')
          _HeartBeatTime = Date.now()
          resolve()
        }
      }, 1000)
    })
  }
  await waitChannelReady()
  clearInterval(waitIntervalId_2)
}

const beginSync = async () => {
  logger.debug('begin sync')
  await waitForChannelReady()

  logger.debug('sync offline data')
  node.services.pubsub.publish(`${_Did.id}-channel`, new TextEncoder().encode(JSON.stringify({ type: 'offline-data-req' })));
}

export const unregister = async () => {
  node.services.pubsub.publish(`${_Did.id}-channel`, new TextEncoder().encode(JSON.stringify({ type: 'unregister' })));
}

const _announceProfile = () => {
  let body = {
    type: 'update-profile',
    payload: { did: _Profile.did, nickName: _Profile.nickName, avatar: _Profile.avatar }
  }
  node.services.pubsub.publish(`${_Did.id}-channel`, new TextEncoder().encode(JSON.stringify(body)));
}

const updateLocalProfile = async (profile) => {
  _DB.saveProfile(profile)
}

export const getProfile = async () => {
  logger.debug('get profile')
  return _Profile
}

const _profileCache = {}
export const fetchProfile = async (targetDid) => {
  if (_profileCache[targetDid]) return _profileCache[targetDid]

  if (!isOnline()) return

  logger.debug('fetch profile', targetDid)
  try {
    let profile = await _fetchProfile(targetDid)
    if (profile) {
      // if friend, update
      let friend = await _DB.getFriend(profile.did)
      if (friend && (friend.nickName != profile.nickName || friend.avatar != profile.avatar)) {
        friend.nickName = profile.nickName
        friend.avatar = profile.avatar
        saveFriend(profile)
      }
      if (profile.avatar) {
        cacheAvatar(profile.avatar)
      }
      // cache profile
      _profileCache[targetDid] = profile
      return profile
    }
  } catch (err) {

  }
}

const cacheAvatar = async (avatar) => {
  let file = await getCacheFile(avatar)
  if (!file) {
    let buffer = await getFileData(avatar)
    if (buffer) {
      // CONVERT to base64
      _DB.cacheFile(avatar, 'image', buffer)
    }
  }
}

const _fetchProfile = (targetDid) => {
  return new Promise((resolve, reject) => {
    const timestamp = Date.now()
    const res_topic = 'fetch-profile-res-' + timestamp
    const handler = (profile) => {
      if (profile && profile.did == targetDid) {
        resolve(profile)
        unregisterHandler(res_topic, handler)
      }
    }
    registerHandler(res_topic, handler)

    let body = {
      type: 'fetch-profile',
      payload: {
        timestamp: timestamp,
        targetDid
      }
    }
    node.services.pubsub.publish(`${_Did.id}-channel`, new TextEncoder().encode(JSON.stringify(body)));
    setTimeout(() => {
      reject()
    }, 1000)
  })
}

const handlers = {}
const registerHandler = (topic, handler) => {
  handlers[topic] = handler
}
const unregisterHandler = (topic, handler) => {
  if (handlers[topic]) delete handlers[topic]
}

const messageHandler = async (evt) => {
  const topic = evt.detail.topic
  let message = new TextDecoder().decode(evt.detail.data);
  logger.debug(`node received: ${message} on topic ${topic}`)

  if (topic.endsWith('-channel')) {
    let body = JSON.parse(message)

    if (handlers[body.type]) {
      handlers[body.type](body.payload)
      return
    }

    if (body.type == 'heart-beat-res') {
      _HeartBeatTime = Date.now()
    } else if (body.type == 'offline-invitations') {
      handleOfflineInvitations(body.payload)
    } else if (body.type == 'offline-chat-messages') {
      handleOfflineChatMessages(body.payload)
    } else if (body.type == 'invitation') {
      handleInvitation(body.payload)
    } else if (body.type == 'chat') {
      handleChatMessage(body.payload)
    }
  }
}
const handleMessages = () => {
  node.services.pubsub.addEventListener('message', messageHandler)
}

const handleOfflineInvitations = async (list) => {
  // sort by time
  list.sort((a, b) => a.time - b.time)

  const mapper = async (item) => {
    const findInvite = await _DB.getInvitation(item.id)
    if (!findInvite) {
      await _DB.saveInvitation(item)
      if (item.status == 'invite') {
        if (globalInvitationListener) {
          globalInvitationListener(item)
        }
      }
    } else if (findInvite.invitor == _Did.id && findInvite.status == 'invite' && item.status == 'confirmed') {
      finishInvitation(item.id, item)
    }
  }
  pMap(list, mapper, { concurrency: 4 })

  // notify clear
  node.services.pubsub.publish(`${_Did.id}-channel`, new TextEncoder().encode(JSON.stringify({
    type: 'offline-invitations-clear',
    payload: {}
  })));
}

let globalInvitationListener
export const setGlobalInvitationListener = (listener) => {
  globalInvitationListener = listener
}

const handleInvitation = async (invitation) => {
  let invitationList = await _DB.getAllInvitations()
  const findInvite = invitationList.find(invite => invite.id == invitation.id)
  if (!findInvite) {
    let findInvite2 = invitationList.find(item => item.type == invitation.type && item.status == 'invite' && item.invitor == invitation.invitor)
    if (findInvite2) {
      logger.debug('invitation already received, delete old one')
      await _DB.deleteInvitation(findInvite2.id)
    }

    await _DB.saveInvitation(invitation)
  } else if (findInvite.invitor == _Did.id && findInvite.status == 'invite' && invitation.status == 'confirmed') {
    finishInvitation(invitation.id, invitation)
  }

  // cache avatar
  if (invitation.status == 'invite' && invitation.invitorAvatar) {
    let buffer = await getFileData(invitation.invitorAvatar)
    if (buffer)
      // CONVERT to base64
      _DB.cacheFile(invitation.invitorAvatar, 'image', buffer)
  }
  if (globalInvitationListener) {
    globalInvitationListener(invitation)
  }
}

const handleOfflineChatMessages = async (list) => {
  // sort by time
  list.sort((a, b) => a.time - b.time)
  // pEachSeries(list, (item) => handleChatMessage(item))
  for (let item of list) {
    await handleChatMessage(item)
  }

  // notify clear
  node.services.pubsub.publish(`${_Did.id}-channel`, new TextEncoder().encode(JSON.stringify({
    type: 'offline-chat-messages-clear',
    payload: {}
  })));
}

const chatListeners = {}
export const addChatListener = (chatId, handler) => {
  chatListeners[chatId] = handler
}

export const removeChatListener = (chatId, handler) => {
  delete chatListeners[chatId]
}

let globalChatListener
export const setGlobalChatListener = (listener) => {
  globalChatListener = listener
}

const handleChatMessage = async (chatData) => {
  const { id, chatId, isGroup, type, hash, did, message, nickName, avatar } = chatData

  chatData.time = dayjs().format('YYYYMMDD HHmmss')

  let existMsg = await _DB.getChatMessage(id)
  if (existMsg) return

  logger.debug(`handle chat message from ${nickName}`)

  let chatItem = await _DB.getChatItem(chatId)
  if (!chatItem) {
    logger.debug('it"s new chat')
    if (isGroup) {
      logger.debug('its group')
      chatItem = {
        id: chatId,
        type: isGroup ? 'group' : 'friend',
      }
      // get group info
      if (type == 'admin') {
        const { name, admin, members } = JSON.parse(message)
        chatItem.name = name
        chatItem.members = members
        chatItem.admin = admin
      } else {
        logger.debug("it's first group chat, but not admin type")
        return
      }
    } else {
      logger.debug('its friend, but friend chat not found, discard this message ')
      return
    }
  }

  let chatLister = chatListeners[chatId]
  // decrypt message
  if (type == 'txt' || type == 'file') {
    logger.debug('decrypt message')
    chatData.message = await decryptJWE(ipfs, _Did, hash)
    logger.debug('chat message', chatData.message)
    chatData.isRead = chatLister ? true : false
    chatData.isDelete = false

    if (type == 'file') {
      try {
        let buffer = await getFileData(chatData.message)
        if (buffer) {
          logger.debug('cache file')
          _DB.cacheFile(chatData.message, 'file', buffer)
        }
      } catch (err) {
        logger.debug('err when get file data', chatData.message)
      }
    }
    await _DB.saveChatMessage(chatData)
    await setChatVisible(chatId)
  } else if (isGroup && type == 'admin') {
    logger.debug("it's admin type, handle memebership")
    let oldAdmin = chatItem.admin
    // handleMembershipChange(chatItem, did, message)
    const { type: adminType, admin, members, membersChanged } = JSON.parse(message)
    chatItem.members = members
    chatItem.admin = admin
    if (adminType == 'kickoff') {
      if (membersChanged.find(item => item.did == _Did.id)) {
        logger.debug('you are kickoff')
        chatItem.members = [_Did.id]
        chatItem.admin = null
        // chatItem.name = name
        await _DB.saveChatMessage(chatData)
        await setChatVisible(chatId)
      }
    } else if (adminType == 'join') {
      await _DB.saveChatMessage(chatData)
      await setChatVisible(chatId)
    } else if (adminType == 'leave') {
      if (membersChanged.find(item => item.did == oldAdmin) && admin == _Did.id) {
        // 原管理员退出，你成为新管理员
        await _DB.saveChatMessage(chatData)
        await setChatVisible(chatId)
      }
    }
    await _DB.saveChatItem(chatItem)
  }

  _cacheChatData({ did, nickName, avatar })

  // if (type == 'txt' || type == 'file') {
  //   logger.debug('save txt or file message to local')
  //   await _DB.saveChatMessage(chatData)
  // } else if (type == 'admin') {
  //   const { type: adminType, admin, membersChanged } = JSON.parse(message)
  //   if (admin == _Did.id) {
  //     logger.debug('save admin message')
  //     await _DB.saveChatMessage(chatData)
  //   } else {
  //     // check if self is removed from group
  //     if (isGroup && adminType == 'leave' && membersChanged.find(item => item.did == _Did.id)) {
  //       logger.debug('you are kickoff')
  //       chatItem.members = [_Did.id]
  //       chatItem.admin = null
  //       // chatItem.name = name
  //       await _DB.saveChatItem(chatItem)

  //       let message = `你已被“${nickName}”踢出群聊`
  //       let data = {
  //         id: uuidv4(), chatId, isGroup, type: 'system', message, did, time: chatData.time, isRead: false, isDelete: false, nickName, avatar
  //       }
  //       _DB.saveChatMessage(data)
  //     }
  //   }
  // }

  if (chatLister) {
    logger.debug("it's listening, git it to handle")
    chatLister(chatData)
  } else if (globalChatListener) {
    logger.debug("global listening, git it to handle")
    globalChatListener(chatData)
  }
}

const _cacheChatData = async ({ did, nickName, avatar }) => {
  // if friend, update nickName and avatar
  let friend = await _DB.getFriend(did)
  if (friend) {
    logger.debug('it"s friend, update it')
    if (friend.nickName != nickName || friend.avatar != avatar) saveFriend({ did, nickName, avatar })
  } else if (avatar) {
    cacheAvatar(avatar)
  }
}

const announceMemeberChat = async (findChat, targetMembers, members, type) => {
  let chatMembers = await _getChatMembersInfo(findChat)
  let membersChanged = []
  for (let did of members) {
    let findMem = chatMembers.find(item => item.did == did)
    if (findMem) {
      membersChanged.push({ did: findMem.did, nickName: findMem.nickName, avatar: findMem.avatar })
    } else {
      membersChanged.push({ did })
    }
  }

  let chatMessage = {
    id: uuidv4(),
    time: dayjs().format('YYYYMMDD HHmmss'),
    chatId: findChat.id,
    isGroup: true,
    type: 'admin',
    ..._Profile,
    message: JSON.stringify({ type, name: findChat.name, admin: findChat.admin, members: findChat.members, membersChanged }),
  }

  delete chatMessage.image
  if (type == 'join') {
    await _DB.saveChatMessage(chatMessage)
  }

  chatMessage.members = targetMembers
  // if (type == 'join') {
  //   chatMessage.members = findChat.members
  // } else if (type == 'leave') {
  //   chatMessage.members = [findChat.admin]
  // }

  let body = {
    type: 'chat',
    payload: chatMessage
  }
  logger.debug('announce members change', chatMessage)
  node.services.pubsub.publish(`${_Did.id}-channel`, new TextEncoder().encode(JSON.stringify(body)));
}

export const resendChatMessage = async (chatId, msgId) => {
  logger.debug('resend chat message', chatId, msgId)

  let chatItem = await _DB.getChatItem(chatId)
  logger.debug('chat item', chatItem)
  if (!chatItem) {
    return
  }

  let chatData = await _DB.getChatMessage(msgId)
  logger.debug('chat data', chatData)

  if (!isOnline() || chatItem.members.length == 1 || !chatData) return

  logger.debug('resend message')

  delete chatData.status
  delete chatData.reason
  chatData.time = dayjs().format('YYYYMMDD HHmmss')
  await _DB.saveChatMessage(chatData)

  if (chatData.type == 'txt') {
    // re encrypt
    let targetDids = chatItem.members
    if (chatItem.type == 'friend') {
      targetDids = chatId.split('-')
    }
    logger.debug('targit dids', targetDids)
    const jweCid = await addEncryptedData(ipfs, _Did, [...targetDids], chatData.message)
    let hash = jweCid.toString()
    logger.debug('hash', hash)
    chatData.hash = hash
  }

  delete chatData.message
  // delete chatData.isRead
  delete chatData.isDelete
  let chatMessage = {
    ...chatData,
    members: chatItem.members,
  }
  let body = {
    type: 'chat',
    payload: chatMessage
  }
  node.services.pubsub.publish(`${_Did.id}-channel`, new TextEncoder().encode(JSON.stringify(body)));

  return chatData
}

export const sendChatMessage = async (chatId, message, type) => {
  let chatItem = await _DB.getChatItem(chatId)
  if (!chatItem) return

  await addFriendChatItemFromChatId(chatId)
  await setChatVisible(chatId)

  let targetDids = chatItem.members
  if (chatItem.type == 'friend') {
    targetDids = chatId.split('-')
  }
  logger.debug('targit dids', targetDids)
  const jweCid = await addEncryptedData(ipfs, _Did, [...targetDids], message)
  let hash = jweCid.toString()
  logger.debug('put cid to db, ' + hash)

  let isGroup = type == 'friend' ? false : true
  let id = uuidv4()
  let time = dayjs().format('YYYYMMDD HHmmss')
  logger.debug('send time', time)
  let chatData = {
    id, chatId, isGroup, type: 'txt', message, time, isRead: true, isDelete: false, ..._Profile, hash
  }
  delete chatData.image

  if (!isOnline()) {
    chatData.status = 'not_send'
    chatData.reason = 'network_offline'
    await _DB.saveChatMessage(chatData)
    return chatData
  }
  if (chatItem.members.length == 1) {
    chatData.status = 'not_send'
    chatData.reason = 'no_members'
    await _DB.saveChatMessage(chatData)
    return chatData
  }
  await _DB.saveChatMessage(chatData)

  delete chatData.message
  let chatMessage = {
    ...chatData,
    members: chatItem.members,
  }
  let body = {
    type: 'chat',
    payload: chatMessage
  }
  node.services.pubsub.publish(`${_Did.id}-channel`, new TextEncoder().encode(JSON.stringify(body)));

  return chatData
}

export const sendChatFile = async (chatId, file, type) => {
  let chatItem = await _DB.getChatItem(chatId)
  if (!chatItem) return

  await addFriendChatItemFromChatId(chatId)
  await setChatVisible(chatId)

  let fileCid = await ipfs.add(file)
  let fileHash = fileCid.cid.toString()
  logger.debug('file hash', fileHash)

  const jweCid = await addEncryptedData(ipfs, _Did, chatItem.members, fileHash)
  let hash = jweCid.toString()
  logger.debug('put cid to db, ' + jweCid.toString())

  const buffer = await getFileData(fileHash)
  _DB.cacheFile(fileHash, 'file', buffer)

  let isGroup = type == 'friend' ? false : true
  let id = uuidv4()
  let time = dayjs().format('YYYYMMDD HHmmss')
  let chatData = {
    id, chatId, isGroup, type: 'file', message: fileHash, ..._Profile, time, isRead: true, isDelete: false,
    fileName: file.name, fileType: file.type, hash
  }
  delete chatData.image

  if (!isOnline()) {
    chatData.status = 'not_send'
    chatData.reason = 'network_offline'
    await _DB.saveChatMessage(chatData)
    return chatData
  }
  if (chatItem.members.length == 1) {
    chatData.status = 'not_send'
    chatData.reason = 'no_members'
    await _DB.saveChatMessage(chatData)
    return chatData
  }
  await _DB.saveChatMessage(chatData)

  delete chatData.message
  let chatMessage = {
    ...chatData,
    members: chatItem.members,
  }
  let body = {
    type: 'chat',
    payload: chatMessage
  }
  node.services.pubsub.publish(`${_Did.id}-channel`, new TextEncoder().encode(JSON.stringify(body)));

  return chatData
}


export const getAllChatData = async (chatId) => {
  logger.debug('get all chat messages')
  let result = await _DB.getChatMessages(chatId)
  // replace latest nickName and avatar
  let chatInfo = await getChatInfo(chatId)
  if (chatInfo) {
    let latestMembers = chatInfo.members
    for (let msg of result) {
      let findMem = latestMembers.find(item => item.did == msg.did)
      if (findMem) {
        msg.nickName = findMem.nickName
        msg.avatar = findMem.avatar
      }
    }
  }
  logger.debug('chat data', result)
  return result
}

const saveFriend = async (friend) => {
  logger.debug('save friend', friend)
  // updateFriendList(friend)
  await _DB.saveFriend(friend)
  if (friend.avatar) {
    await getCacheFile(friend.avatar)
    // let buffer = await getFileData(friend.avatar)
    // if (buffer) {
    //   // CONVERT to base64
    //   _DB.cacheFile(friend.avatar, 'image', buffer)
    // }
  }
}

export const connectRelay = async (relayAddress) => {
  logger.debug('connecting to relay', relayAddress)
  // await node.hangUp(multiaddr(relayAddress))

  const addr = await node.dial(multiaddr(relayAddress))
  logger.debug('connected to relay', addr)
  const address = node.getMultiaddrs()
  for (let item of address) {
    logger.debug('relay address', item.toString())
    // relay address /dns4/webrtc.we-linkin.com/tcp/443/wss/p2p-webrtc-star/p2p/12D3KooWNAvDa9tt5pnmTEsajx4j8TDXieAH5B3PGBiG1C7VJQkQ
  }
}

export const inviteFriend = async (friendDid) => {
  if (friendDid) {
    let profile = await fetchProfile(friendDid)
    console.log('profile', profile)
    addInvite('friend', friendDid)
    return true
  } else {
    return false
  }
}

export const addInvite = async (type, friendDid) => {
  logger.debug('add invite', type, friendDid)
  if (!isOnline()) {
    logger.debug('offline, cancel operation')
    return
  }

  let invitationList = await _DB.getAllInvitations()
  let findInvite = invitationList.find(item => item.type == type && item.status == 'invite' && item.invitee == friendDid)
  if (findInvite) {
    logger.debug('invitation already send, delete old one')
    await _DB.deleteInvitation(findInvite.id)
  }

  const id = uuidv4()
  const inviteRecord = {
    id,
    type,
    invitor: _Profile.did,
    invitee: friendDid,
    status: 'invite',
    // action: '',
    // confirmTime: '',
    time: dayjs().format('YYYYMMDD HHmmss'),
    invitorNickName: _Profile.nickName,
    invitorAvatar: _Profile.avatar
  }

  let body = {
    type: 'invitation',
    payload: inviteRecord
  }
  node.services.pubsub.publish(`${_Did.id}-channel`, new TextEncoder().encode(JSON.stringify(body)))

  let friend = await fetchProfile(friendDid)
  if (friend) {
    inviteRecord.inviteeNickName = friend.nickName
    inviteRecord.inviteeAvatar = friend.avatar
  }

  await _DB.saveInvitation(inviteRecord)
}

export const confirmInvitation = async (inviteId, action) => {
  logger.debug('confirm invite', inviteId, action)
  if (!isOnline()) {
    logger.debug('offline, cancel operation')
    return
  }

  const findInvite = await _DB.getInvitation(inviteId)
  if (!findInvite) return

  findInvite.status = 'confirmed'
  findInvite.action = action
  findInvite.confirmTime = dayjs().format('YYYYMMDD HHmmss')
  findInvite.time = dayjs().format('YYYYMMDD HHmmss')
  findInvite.inviteeNickName = _Profile.nickName
  findInvite.inviteeAvatar = _Profile.avatar

  let body = {
    type: 'invitation',
    payload: findInvite
  }
  delete body.payload.isRead
  node.services.pubsub.publish(`${_Did.id}-channel`, new TextEncoder().encode(JSON.stringify(body)))

  findInvite.status = 'done'
  await _DB.saveInvitation(findInvite)

  if (action == 'accept') {
    let friend = { did: findInvite.invitor, nickName: findInvite.invitorNickName, avatar: findInvite.invitorAvatar }
    await saveFriend(friend)
    await createFriendChatByInvitee(friend)
  }
}

const createFriendChatByInvitee = async (friend) => {
  await addFriendChatItem(friend)
  // add system chat message
  let chatId = [friend.did, _Profile.did].sort().join('-')
  let time = dayjs().format('YYYYMMDD HHmmss')
  let message = `你已添加了${friend.nickName}，现在可以开始聊天了`
  let chatData = {
    id: uuidv4(), chatId, isGroup: 'false', type: 'system', message, ..._Profile, time
  }
  delete chatData.image
  _DB.saveChatMessage(chatData)
}

const createFriendChatByInvitor = async (friend) => {
  await addFriendChatItem(friend)
  // add system chat message
  let chatId = [friend.did, _Did.id].sort().join('-')
  let time = dayjs().format('YYYYMMDD HHmmss')

  let message = '我通过了你的朋友验证请求，现在我们可以开始聊天了'
  let chatData = {
    id: uuidv4(), chatId, isGroup: 'false', type: 'txt', message, did: friend.did, time, isRead: false, isDelete: false, nickName: friend.nickName, avatar: friend.avatar
  }
  _DB.saveChatMessage(chatData)
}

export const finishInvitation = async (inviteId, invitation) => {
  logger.debug('friend confirmed, add friend')

  const findInvite = await _DB.getInvitation(inviteId)
  if (!findInvite) return

  // add friend
  if (invitation.action == 'accept') {
    let friend = { did: findInvite.invitee, nickName: invitation.inviteeNickName, avatar: invitation.inviteeAvatar }
    await saveFriend(friend)
    await createFriendChatByInvitor(friend)

    if (invitation.inviteeNickAvatar) {
      let buffer = await getFileData(invitation.inviteeNickAvatar)
      if (buffer) {
        // CONVERT to base64
        _DB.cacheFile(invitation.inviteeNickAvatar, 'image', buffer)
      }
    }
  }

  // update invite
  findInvite.status = 'done'
  findInvite.inviteeNickName = invitation.inviteeNickName
  findInvite.inviteeNickAvatar = invitation.inviteeNickAvatar

  // save to local
  await _DB.saveInvitation(findInvite)
}

export const deleteInvite = async (inviteId) => {
  await _DB.deleteInvitation(inviteId)
}

const _updateFriendData = async () => {
  if (!isOnline()) return
  let friendList = await _DB.getAllFriends()
  const mapper = async (friend) => {
    let newFriend = await fetchProfile(friend.did)
    if (newFriend) {
      if (newFriend.nickName != friend.nickName || newFriend.avatar != friend.avatar) {
        friend.nickName = newFriend.nickName
        friend.avatar = newFriend.avatar
        saveFriend(friend)
      }

      if (newFriend.nickName != friend.nickName) {
        // update friend chat
        let chatId = [_Did.id, friend.did].sort().join('-')
        let chatItem = await _DB.getChatItem(chatId)
        if (chatItem) {
          chatItem.nickName = friend.nickName
          chatItem.name = friend.nickName
          _DB.saveChatItem(chatItem)
        } else {
          addFriendChatItem(friend)
        }
      }
    }
  }
  pMap(friendList, mapper, { concurrency: 4 })
}

export const getMyFriendsData = async () => {
  let friendList = await _DB.getAllFriends()
  return friendList
}

export const deleteFriend = async (friendDid) => {
  await _DB.deleteFriend(friendDid)

  // leave chat
  leaveGroupChat([friendDid, _Did.id].sort().join('-'))
}

export const getInvitationsData = async () => {
  let invitationList = await _DB.getAllInvitations()
  if (isOnline()) {
    const mapper = async (invite) => {
      if (invite.status == 'invite') {
        let profile = await fetchProfile(invite.invitor)
        if (profile && (profile.nickName != invite.invitorNickName || profile.avatar != invite.invitorAvatar)) {
          invite.invitorNickName = profile.nickName
          invite.invitorAvatar = profile.avatar
          _DB.saveInvitation(invite)
          cacheAvatar(profile.avatar)
        }
      }
    }
    await pMap(invitationList, mapper, { concurrency: 4 })
  }

  return invitationList
}

export const uploadAvatar = async (file) => {
  if (!isOnline()) {
    logger.debug('offline, cancel operation')
    return
  }

  logger.debug('uploading avatar')

  let hash = await ipfs.add(file)
  logger.debug('file hash', hash.cid.toString())

  let avatar = hash.cid.toString()

  let avatarFile = await getFileData(avatar)
  // CONVERT to base64
  _DB.cacheFile(avatar, 'image', avatarFile)

  _Profile.avatar = avatar
  updateLocalProfile(_Profile)

  _announceProfile()

  return avatar
}

export const changeSelfNickName = async (name) => {
  logger.debug('change nickName', name)
  _Profile.nickName = name
  updateLocalProfile(_Profile)

  _announceProfile()
}

export const getFileData = async (cid) => {
  logger.debug('get file data', cid)
  const content = []
  try {
    for await (const chunk of ipfs.cat(cid, { timeout: 10000 })) {
      content.push(chunk);
    }
  } catch (err) {
    logger.error('failed to fetch data from ipfs')
  }

  let buffer = Buffer.concat(content);
  return buffer
}

const addFriendChatItem = async (friend) => {
  let chatId = [friend.did, _Did.id].sort().join('-')
  let chatItem = { id: chatId, name: friend.nickName, avatar: friend.avatar, type: 'friend', members: [friend.did, _Did.id], visible: true }
  await _DB.saveChatItem(chatItem)
}

const addFriendChatItemFromChatId = async (chatId) => {
  let chatItem = await _DB.getChatItem(chatId)
  if (chatItem) return

  let members = chatId.split('-')
  let friendDid = members.find(mem => mem != _Did.id)
  let friend = await _DB.getFriend(friendDid)
  if (friend)
    await addFriendChatItem(friend)
}

export const getChatListData = async () => {
  let chatList = await _DB.getAllChatList()

  const mapper = async (chatItem) => {
    let chatInfo = await _getChatInfo(chatItem)
    chatItem.chatInfo = chatInfo
  }
  await pMap(chatList, mapper, { concurrency: 4 })

  logger.debug('chat list', chatList)
  return chatList
}

export const deleteChatMessage = async (chatId, id) => {
  await _DB.deleteChatMessage(id)
}

export const markRead = async (chatId, id) => {
  await _DB.markRead(id)
}

export const markDelete = async (chatId, id) => {
  await _DB.markDelete(id)
}

export const setChatInvisible = async (chatId) => {
  logger.debug('set chat invisible', chatId)
  let chatItem = await _DB.getChatItem(chatId)
  if (!chatItem) return
  chatItem.visible = false
  await _DB.saveChatItem(chatItem)
  return true
}

export const setChatVisible = async (chatId) => {
  logger.debug('set chat visible', chatId)
  let chatItem = await _DB.getChatItem(chatId)
  if (!chatItem) return
  chatItem.visible = true
  await _DB.saveChatItem(chatItem)
  return true
}

export const leaveGroupChat = async (chatId) => {
  logger.debug('leave chat group', chatId)
  if (!isOnline()) {
    logger.debug('offline, cancel operation')
    return
  }

  let findChat = await _DB.getChatItem(chatId)
  let targetMembers = [...findChat.members]

  let index = findChat.members.findIndex(item => item == _Did.id)
  if (index != -1) {
    findChat.members.splice(index, 1)
  }

  // if admin
  if (findChat.admin == _Did.id && findChat.members.length > 0) {
    findChat.admin = findChat.members[0]
  }

  // broadcase member change
  if (findChat.members.length > 0) {
    await announceMemeberChat(findChat, targetMembers, [_Did.id], 'leave')
  }

  await _DB.deleteChatItem(chatId)
}

export const removeMembersFromChatGroup = async (chatId, members) => {
  logger.debug('remove members from chat group', chatId, members)
  if (!isOnline()) {
    logger.debug('offline, cancel operation')
    return
  }

  let findChat = await _DB.getChatItem(chatId)
  let targetMembers = [...findChat.members]
  for (let mem of members) {
    let index = findChat.members.findIndex(item => item == mem)
    if (index != -1) {
      findChat.members.splice(index, 1)
    }
  }
  await _DB.saveChatItem(findChat)
  // broadcase member change
  announceMemeberChat(findChat, targetMembers, members, 'kickoff')
}

export const changeGroupChatName = async (chatId, name) => {
  logger.debug('change group name', chatId, name)
  let chatItem = await _DB.getChatItem(chatId)
  if (chatItem) {
    chatItem.name = name
    _DB.saveChatItem(chatItem)
  }
}

export const addMembersToChatGroup = async (chatId, members) => {
  logger.debug('add members to chat group', chatId, members)

  let findChat = await _DB.getChatItem(chatId)
  for (let mem of members) {
    let index = findChat.members.findIndex(item => item == mem)
    if (index == -1) {
      findChat.members.push(mem)
    }
  }
  await _DB.saveChatItem(findChat)
  // broadcase member change
  announceMemeberChat(findChat, findChat.members, members, 'join')
}

export const createChatGroup = async (name, members) => {
  logger.debug('create chat group', name, members)
  if (!isOnline()) {
    logger.debug('offline, cancel operation')
    return
  }

  if (!members.find(item => item == _Did.id)) {
    members.unshift(_Did.id)
  }

  let chatItem = {
    id: uuidv4(),
    type: 'group',
    name,
    members,
    admin: _Did.id,
    visible: true
  }
  await _DB.saveChatItem(chatItem)

  // broadcase member change
  await announceMemeberChat(chatItem, chatItem.members, members, 'join')

  return chatItem
}

export const getCacheFile = async (hash) => {
  let cacheFile = await _DB.getCacheFile(hash)
  if (cacheFile) return cacheFile.buffer
  else {
    let buffer = await getFileData(hash)
    if (buffer) {
      await _DB.cacheFile(hash, 'image', buffer)
    }
  }
}

export const markInvitationRead = async (id) => {
  logger.debug('mark invitation read', id)
  let findInvite = await _DB.getInvitation(id)
  if (findInvite) {
    findInvite.isRead = true
    await _DB.markInvitationRead(id)
  }
}

export const exportDB = async () => {
  return await _DB.exportDB()
}

const convertToJSON = async (blob) => {
  const decoder = new TextDecoder();
  const chunkSize = 512;
  let text = '';
  const length = blob.size;
  let position = 0;
  while (position < length) {
    const slice = blob.slice(position, position + chunkSize);
    text += decoder.decode(await slice.arrayBuffer());
    position += chunkSize;
  }
  return JSON.parse(text)
}

export const backupDB = async () => {
  logger.debug('backup DB')
  const blob = await _DB.exportDB()
  // console.log(blob)
  let dbContent = await convertToJSON(blob)
  // console.log(dbContent)
  const jweCid = await addEncryptedData(ipfs, _Did, [_Did.id], dbContent)
  let hash = jweCid.toString()
  // let contentId = await ipfs.add(blob)

  const keys = await ipfs.key.list()
  // logger.debug('ipns keys', keys)

  let findKey = keys.find(item => item.name == nodeId)
  if (!findKey) {
    findKey = await ipfs.key.gen(nodeId, {
      type: 'ed25519',
      size: 2048
    })
  }

  logger.debug('backup db hash', hash)
  const res = await ipfs.name.publish(hash, { key: nodeId })
  logger.debug('published to ipfs', res)

  return res
}

export const restoreDB = async () => {
  logger.debug('restore DB')
  const keys = await ipfs.key.list()

  let findKey = keys.find(item => item.name == nodeId)
  if (!findKey) {
    logger.debug('cannot find key, please register it first')
    return false
  }

  try {
    let ipnsName
    for await (const name of ipfs.name.resolve(findKey.id)) {
      ipnsName = name
      // /ipfs/QmULjXLLJxehXM2xH9awf39c7PZUQRTtvectDqBHMvhNjF
    }
    logger.debug('ipns name', ipnsName)
    let hash = ipnsName.split('/')[2]
    logger.debug('db content hash', hash)

    let dbContent = await decryptJWE(ipfs, _Did, hash)
    console.log('db content restored', dbContent)
    // let dbContent = await getFileData(hash)
    let blob = new Blob([JSON.stringify(dbContent)], {
      type: 'text/plain'
    })
    await _DB.importDB(blob)
    return true
  } catch (err) {
    logger.debug('err restoring db', err)
    return false
  }
}

export const searchMessages = async (chatId, query) => {
  logger.debug('search messages', chatId, query)
  let result = await _DB.searchMessages(chatId, query)
  return result
}

export const getChatInfo = async (chatId) => {
  logger.debug('get chat info', chatId)
  let chatItem = await _DB.getChatItem(chatId)
  if (chatItem) {
    let chatInfo = await _getChatInfo(chatItem)
    logger.debug('chat info', chatInfo)
    return chatInfo
  }
}

const _updateMemberInfo = (list, info) => {
  let findMem = list.find(item => item.did == info.did)
  if (!findMem) {
    list.push(info)
  } else {
    findMem.nickName = info.nickName
    findMem.avatar = info.avatar
  }
}

// get members info from chat messages and friends
const _getChatMembersInfo = async (chatItem) => {
  logger.debug('gather chat members info', chatItem.id)
  let allMessages = await _DB.getChatMessages(chatItem.id)
  allMessages.sort((a, b) => a.time - b.time)
  let chatMembers = [] // { did, nickName, avatar }
  for (let msg of allMessages) {
    let info = { did: msg.did, nickName: msg.nickName, avatar: msg.avatar, msgId: msg.id }
    _updateMemberInfo(chatMembers, info)
    if (msg.type == 'admin') {
      let adminMessage = JSON.parse(msg.message)
      if (adminMessage.membersChanged) {
        for (let memInfo of adminMessage.membersChanged) {
          _updateMemberInfo(chatMembers, memInfo)
        }
      }
    }
  }

  let copyInfo = (target, source) => {
    target.nickName = source.nickName
    target.avatar = source.avatar
  }

  // update info from friends
  for (let mem of chatMembers) {
    if (mem.did == _Did.id) {
      copyInfo(mem, _Profile)
    } else {
      let friend = await _DB.getFriend(mem.did)
      if (friend) {
        copyInfo(mem, friend)
      }
    }
  }

  // add chatItem.members
  if (chatItem) {
    for (let did of chatItem.members) {
      let findMem = chatMembers.find(cmem => cmem.did == did)
      if (!findMem) {
        let friend = await _DB.getFriend(did)
        if (friend) {
          chatMembers.push({ did: friend.did, nickName: friend.nickName, avatar: friend.avatar })
        } else {
          chatMembers.push({ did })
        }
      }
    }
  }
  logger.debug('chat members info', chatMembers)
  return chatMembers
}

const _getChatInfo = async (chatItem) => {
  logger.debug('get chat info', chatItem.id)

  let chatMembers = await _getChatMembersInfo(chatItem)
  let members = []
  for (let mem of chatItem.members) {
    if (isOnline()) {
      let profile = await fetchProfile(mem)
      if (profile) {
        members.push({ did: profile.did, nickName: profile.nickName, avatar: profile.avatar })
        let findMem = chatMembers.find(item => item.did == mem)
        // if (findMem && findMem.msgId && (findMem.nickName != profile.nickName || findMem.avatar != profile.avatar)) {
        //   // update msg nickName and avatar
        //   updateChatMessageProfile(findMem.msgId, profile)
        // }
      } else {
        let findMem = chatMembers.find(item => item.did == mem)
        if (findMem) {
          members.push(findMem)
        } else {
          members.push({ did: mem })
        }
      }
    } else {
      let findMem = chatMembers.find(item => item.did == mem)
      if (findMem) {
        members.push(findMem)
      } else {
        members.push({ did: mem })
      }
    }
  }
  let latestMsg = await _DB.getLastChatMessage(chatItem.id)
  let unReadCount = latestMsg.filter(item => !item.isRead).length
  return {
    id: chatItem.id,
    admin: chatItem.admin,
    members,
    type: chatItem.type,
    name: chatItem.name,
    latestMsg,
    unReadCount,
  }
}

export const searchChats = async (query) => {
  logger.debug('search chats', query)
  let chatList = await _DB.getAllChatList()
  let result = []
  for (let chatItem of chatList) {
    let chatInfo = await _getChatInfo(chatItem)
    // logger.debug('chat info', chatInfo)
    let membersStr = chatInfo.members.map(mem => mem.nickName).join(',')
    if (chatInfo.name.indexOf(query) != -1 || membersStr.indexOf(query) != -1) result.push(chatItem)
  }
  return result
}

window.im_lib = {
  closeNetwork,
  generateMnemonic,
  peerIdToDid,
  addEncryptedData, decryptJWE,
  uint8ArrayToString, uint8ArrayFromString,
  createChatGroup, addMembersToChatGroup, removeMembersFromChatGroup,
  deleteFriend, changeSelfNickName,
  leaveGroupChat, changeGroupChatName,
  inviteFriend, confirmInvitation, finishInvitation,
  getAllChatData,
  sendChatMessage, sendChatFile, resendChatMessage,
  getFileData, getCacheFile,
  setGlobalChatListener, setGlobalInvitationListener,
  addChatListener, removeChatListener,
  getChatListData,
  getMyFriendsData, getInvitationsData,
  markRead, markDelete,
  uploadAvatar, unregister,
  getProfile, fetchProfile, isValidDid,
  isOnline,
  markInvitationRead,
  backupDB, restoreDB,
  restoreNetwork,
  setChatInvisible, setChatVisible,
  searchMessages, searchChats, getChatInfo,
  login, initialize
}